Imports System.Diagnostics
Imports CacheMyWork.NativeMethods
Imports CacheMyWork.UserConfiguration

Public NotInheritable Class ProcessesBusinessLogic

#Region " Variables "

    ' TODO: Consider changing this variable from a shared variable to one that will always die once its local function caller dies
    Friend Shared Process As System.Diagnostics.Process

    ' HACK: Create a second collection to be able to look up a Process by the displayed string in the UIForm checkedlistbox
    ' TODO: !!! implement DataMember connections from the relevant collections to the UI
    Friend Shared ProcessLookupTable As New Collection

    ''' <summary>
    ''' A global Hashtable of the Process objects found on the system, from which the calling application can select those that can be cached by the user.
    ''' </summary>
    ''' <remarks>
    ''' This Hashtable is instantiated once and endures for the lifetime of this application.
    ''' 
    ''' There are occasions when two or more processes with the same name will be stored in this Hashtable; in such cases, the 'key' used to index the entries will differ by a numbered suffix for the 2nd and subsequent entries.
    ''' </remarks>
    Friend Shared EnumeratedProcesses As System.Collections.Hashtable

#End Region

#Region "Public Methods"

    ''' <summary>
    ''' Parent function that calls all the atomic functions in this class, when used in the CacheMyWork application.
    ''' </summary>
    ''' <remarks></remarks>
    Public Shared Sub BuildCollectionOfUserProcesses()

        ' Instantiate the EnumeratedProcesses collection:
        EnumeratedProcesses = New System.Collections.Hashtable()

        ' First create the collection of process structures:
        BuildProcessesCollection()

        ' TODO: !!! Figure out how to get a "dynamic" collection object that is the "current subset of EnumeratedProcesses members that have .IsVisible = True", and pass in that Collection instead of EnumeratedProcesses

        ' Remove all processes that do not have any Windows (visible or invisible):
        HideProcessesWithoutWindows(EnumeratedProcesses)

        ' Remove all processes that do not create a visible Window:
        If DisplayNonVisibleWindows = False Then
            'TODO: investigate why this call removes all remaining processes, so that I can re-enable it
            ' HideProcessesWithoutVisibleWindows(EnumeratedProcesses)
        End If

        ' TODO: implement some sort of LogWriter that passes its results back up to the calling code
        'LogFile.WriteLine("========= Finished removing processes that do not have a Window @ " + Date.Now)

        ' TODO: change this to "HideProcessesSelectedByUserConfiguration" or something similar
        ' Remove the well-known processes that the user (or the default Settings) have decided should never be cached by RunOnce:
        HideWellKnownProcesses(EnumeratedProcesses, HiddenApps)

        ' Remove processes that have already been configured to start in the user's context by various means
        HideProcessesAlreadyAutoStarted(EnumeratedProcesses)

        ' Remove those processes that were not started by the end user, since the user won't be able to (or won't want to) restart such processes next time anyway:
        ' TODO: Determine if there's any difference between the results with and without this "not started by the user" function.
        HideProcessesNotStartedByCurrentUser(EnumeratedProcesses)

        ' TODO: implement some sort of LogWriter that passes its results back up to the calling code
        'LogFile.WriteLine("========= Finished removing processes *not* started by Me @ " + Date.Now)


        ' TODO: implement some sort of LogWriter that passes its results back up to the calling code
        'LogFile.WriteLine("========= Finished creating final collection of processes for display @ " + Date.Now)


        ' TODO: v2 - check whether any of the ProcessesToShowUser are child processes of any other processes in the collection.  If so, Remove the child process.
        ' (e.g. dexplore.exe is a child of devenv.exe, and can't typically be started on its own.)

        ' TODO: v2 - Enumerate all documents held open by the resulting list of Apps, and cache them ahead of time so that the Tool can display them anytime the user selects the App name in the CheckedListBox.
        ' e.g. use process.startinfo.arguments for some apps

        ' TODO: v3 - Look for ways to return to the user's "place" in cached documents (e.g. use bookmarks in Word)

    End Sub

    ''' <summary>
    ''' Constructor to initialize class-wide variables
    ''' </summary>
    ''' <remarks></remarks>
    Private Sub New()

        EnumeratedProcesses = New Hashtable()

    End Sub

#End Region

#Region "Private Methods"

    ''' <summary>
    ''' Enumerate all processes and build the collections of ProcessApplication objects: EnumeratedProcesses, ProcessLookupTable.
    ''' </summary>
    ''' <remarks></remarks>
    Private Shared Sub BuildProcessesCollection()

        For Each Process In Process.GetProcesses
            ' Note: the HashTable Collection's "Key" is created from process' name, so that I can index back into the Collection with just the ProcessName string.
            ' TODO: !!! figure out a more unique index key than ProcessName, since there are many applications that can run more than one instance at a time.

            Dim _CurrentProcess As EnumeratedProcess
            _CurrentProcess = New EnumeratedProcess() ' Re-initialize this variable every pass through the For Each loop

            Try
                ' First make sure you're not doing needless work on the System or Idle "process", which aren't real processes
                If Process.ProcessName = "System" Or Process.ProcessName = "Idle" Then
                    Exit Try
                End If

                ' Create the EnumeratedProcess object to store all needed information about each Process
                ' By enumerating all necessary information up front, CacheMyWork and the user will be able to operate independently of whether the process has subsequently exited

                ' ****************************************************************
                ' Here's where we set all the Properties of the EnumeratedProcess
                ' ****************************************************************

                ' TODO: think again about whether it's necessary to check for the presence of Windows, when we're also checking for the presence of Visible Windows.  Now that CacheMyWork isn't removing entries from a collection, but just tagging them with all metadata, this may not only be redundant but anti-performance.
                If DoesProcessHaveWindows(Process) Then ' Checking whether any Windows (visible or not) are available
                    _CurrentProcess.HasWindow = True
                End If

                ' Set the IsHiddenByUser property according to whether the ProcessName was found in the My.User.Settings array at startup (when it was slurped into the HiddenApps Hashtable)
                _CurrentProcess.IsHiddenByUser = HiddenApps.ContainsKey(Process.ProcessName)

                Try
                    ' TODO: figure out how best to destroy the 'object' that encapsulates the IsWindowVisible API call, since it's exposed as a Shared Function - this is gonna be harder than I thought
                    If IsWindowVisible(Process.Handle) Then
                        _CurrentProcess.IsWindowVisible = True
                    End If

                Catch ex As System.ComponentModel.Win32Exception When ex.NativeErrorCode = 5 ' Only handle the "acces is denied" COM exception for now
                    _CurrentProcess.IsRemovedProgrammatically = True ' If there are any errors accessing a Process this way, then we can safely filter out the process right now
                End Try

                _CurrentProcess.ProcessDescriptiveName = GetProcessDescriptiveName(Process) ' Retrieve the descriptive name for the Process object

                _CurrentProcess.ProcessName = Process.ProcessName ' Retrieve the ProcessName for the Process object
                _CurrentProcess.ProcessOwner = GetProcessOwnerFromWmi(Process) 'Retrieve the UserName for the current Process object

                ' Retrieve the CommandLine for the current Process object
                _CurrentProcess.ProcessCommandLine = GetCommandLine(Process)

                ' Finally, set the artificial state that will be used to subtract out processes one by one
                _CurrentProcess.ProcessWillBeDisplayedToUser = True

                ' Once the ProcessApplication object is created, add it to the ProcessesToShowUser collection, which is the list that we'll be adjusting throughout this procedure.
                EnumeratedProcesses.Add(_CurrentProcess.ProcessName, _CurrentProcess)

                ' TODO: dump this lookup table as soon I'm able
                ' TODO: !!! Is there some way to create a HashTable with *two* indices, not just one?  Then I could index into EnumeratedProcesses with ProcessName or DescriptiveName
                ' HACK: Create a lookup table to be able to lookup ProcessName by Descriptive Name, since I don't have a clear idea how to do this in a Collection of Structures
                'ProcessLookupTable.Add(_CurrentProcess.ProcessDescriptiveName, _CurrentProcess.ProcessName)

                ' TODO: what to do when I encounter two or more instances of IEXPLORE?  Just discard the 2nd and subsequent 
            Catch ex As System.ArgumentException
                ' There are at least two case where this Exception occurs:
                '   (1) when there are two or more Processes of the same name running at the same time (e.g. svchost.exe) = "Add failed.  Duplicate key values supplied."
                '   (2) ex.Message = "Illegal characters in path." (e.g. csrss.exe, winlogon.exe)
                ' Where (2) is the cause, just catch this exception and move on - the assumption being, the important apps to cache for next time only run one at a time.

                ' TODO: implement some sort of LogWriter that passes its results back up to the calling code
                'LogFile.WriteLine("Failed to fully evaluate process " + Process.ProcessName + "because " + ex.Message + " @ " + Date.Now)

                ' In cases where there are two or more processes with the same name, create a unique value for the Hashtable entry to be added
                If ex.Message.StartsWith("Item has already been added") Then
                    ' If it's Internet Explorer, then just discard this second process and move on.  See http://www.codeplex.com/CacheMyWork/Wiki/View.aspx?title=IEXPLORE&referringTitle=Documentation for detailed explanation of why.
                    If _CurrentProcess.ProcessName = "IEXPLORE" Then
                        Exit Try
                    End If

                    ' Append an integer (from 1 to infinity, theoretically) to the ProcessName and try to find that Key in the Hashtable
                    ' When that Key cannot be found in the Hashtable, use that Key to add a new instance of the Process to the Hashtable
                    Dim count As Integer
                    Dim key As String
                    count = 0
                    Do
                        count = count + 1
                        key = _CurrentProcess.ProcessName + count.ToString()
                    Loop Until EnumeratedProcesses.ContainsKey(key) = False

                    EnumeratedProcesses.Add(key, _CurrentProcess)

                End If

            Catch ex As System.ApplicationException
                ' The only known case where this Exception occurs is when the process exits after it's added to the ProcessesToShowUser array
                ' Since we're tracking this state in the EnumeratedProcesses class, we shall toggle the .Exists property to "False"
                _CurrentProcess.Exists = False


            Catch ex As System.InvalidOperationException
                ' This occurs when the process currently being investigated has exited (thrown explicitly by Processes.GetProcessDescriptiveName())
                ' Just best to let it go - when the process has exited, there's no use in trying to add it to the ProcessesToShowUser collection
            End Try

            ' TODO: dispose/finalize/null the _CurrentProcess object?  Would that have any benefit (performance, memory)?
        Next Process
    End Sub

    ''' <summary>
    ''' Determines if the Process has any window objects as far as the operating system is concerned.
    ''' </summary>
    ''' <returns>True if Process has at least one window (visible or not); False if no windows.</returns>
    ''' <remarks></remarks>
    Private Shared Function DoesProcessHaveWindows(ByVal process As Process) As Boolean

        ' Validate input before using
        If process Is Nothing Then
            Throw New ArgumentNullException("process")
            Return False ' Added this line to eliminate the warning "Function doesn't return a value on all code paths."
        End If

        Try
            'MessageBox.Show("Process: " + process.ProcessName + vbCrLf + "MWH: " + process.MainWindowHandle.ToString)
            If process.MainWindowHandle <> 0 Then
                Return True
            Else
                Return False ' Added this line to eliminate the warning "Function doesn't return a value on all code paths."
            End If

        Catch ex As System.ComponentModel.Win32Exception
            ' e.g. "Access Denied" error when Process.MainWindowHandle fails because the user doesn't have sufficient privilege
            ' to the processes they don't own.  This also fails on Vista when user is Protected Admin - only elevated Admin can get past this.
            ' TODO: figure out if I could re-order the functions so that this occurs *after* the RemoveProcessesNotStartedByMe() call, so that this function never tries to open a handle to a "foreign" process.
            Return False ' Added this line to eliminate the warning "Function doesn't return a value on all code paths."

        End Try

    End Function

    Private Shared Function GetCommandLine(ByVal process As Process) As String

        Dim commandLine As String = String.Empty ' initialize local variable to avoid compiler warnings about "not all code paths..."

        Try
            ' TODO: Re-examine the decision to gather the Command Line here up-front, as it's causing potentially many occasions of NullReferenceException for processes that do not have a known Command Line

            ' NOTE: It appears that requesting the Command Line via WMI ends up with a string that has a trailing space character at the end of the string (i.e. after the second " delimiter).
            ' This trailing space (for some unknown reason) causes Windows to fail to execute the Command Lines retrieved from RunOnce entries, so the trailing space must be deleted.
            ' According to MSDN documentation for .TrimEnd(), a null parameter will trim whitespace (aka space characters) only.
            commandLine = GetProcessPropertyFromWmi(process, "CommandLine").TrimEnd()

        Catch ex As System.ArgumentException
            ' There is one known case where this Exception occurs:
            '   (1) when the process (e.g. System) does not have a command line

            ' Rethrow this exception up the stack, so it can be handled more consistently - this immediately exits the Try...End Try routine
            Throw

            ' Visual Studio warns "Function doesn't return a value on all code paths", so I'm returning the empty commandLine here as well (don't know if this is the right way to address the warning or not)
            Return commandLine

        End Try

        Return commandLine
    End Function

    Overloads Shared Function GetProcessDescriptiveName(ByVal processShortName As String) As String
        ' Gets the process' Process object and then calls into GetProcessDescriptiveName(proc as Process)

        Dim _ProcessDescriptiveName As String

        ' Validate input on this non-private method
        If processShortName Is Nothing Then
            Throw New ArgumentNullException("processShortName")
        End If

        Try
            ' TODO: Code here
            ' HACK: set to a value just to eliminate the warning
            _ProcessDescriptiveName = processShortName
            ' Call GetProcessDescriptiveName(proc as Process)

        Catch ex As Exception
            ' Catch 
            Throw

        End Try

        ' TODO: Code here

        Return _ProcessDescriptiveName

    End Function

    Overloads Shared Function GetProcessDescriptiveName(ByVal process As Process) As String
        ' HACK: I don't know why this needs to be Shared - I did that to resolve an Error that Visual Studio gave me.

        Dim _ProcessDescriptiveName As String = String.Empty ' Setting it to String.Empty to avoid VS error "variable is used before it has been assigned a value"
        Dim _ValidatedProcessDescriptiveName As String = String.Empty ' Setting it to String.Empty to avoid VS error "variable is used before it has been assigned a value"

        ' Validate input on this non-private method
        If process Is Nothing Then
            Throw New ArgumentNullException("process")
        End If

        Try
            ' Try to generate a readable Descriptive Name that doesn't contain an empty string (as is not uncommon in some apps for some of these fields)
            ' TODO: Determine why this loop is executing twice in a row on each Process
            If String.IsNullOrEmpty(process.MainModule.FileVersionInfo.FileDescription) = False Then
                _ProcessDescriptiveName = process.MainModule.FileVersionInfo.FileDescription
            ElseIf String.IsNullOrEmpty(process.MainWindowTitle) = False Then
                _ProcessDescriptiveName = process.MainWindowTitle
            ElseIf String.IsNullOrEmpty(process.MainModule.FileVersionInfo.InternalName) = False AndAlso String.IsNullOrEmpty(process.MainModule.FileVersionInfo.ProductName) = False Then
                _ProcessDescriptiveName = process.MainModule.FileVersionInfo.ProductName + " (" + process.MainModule.FileVersionInfo.InternalName + ")"
            ElseIf String.IsNullOrEmpty(process.MainModule.FileVersionInfo.InternalName) = False Then
                _ProcessDescriptiveName = process.MainModule.FileVersionInfo.InternalName
            Else
                ' This should always return a non-empty value
                _ProcessDescriptiveName = process.ProcessName
            End If

        Catch ex As System.ComponentModel.Win32Exception
            ' When the Win32 exception is "Access is denied" (ErrorCode=-2147467259), then we'll have to try a less invasive Property:
            ' TODO: decide what to do on Vista with elevated Processes (which cause the "Access is denied" error) - should they be scheduled for restart?  If so, is there any workaround for the RunOnce mitigation against elevated processes in Vista?

            ' This conditional code assumes Substring > 5 is True for Windows Vista, Windows Server 2008 and beyond, but false for XP & 2003
            If My.Computer.Info.OSVersion.Substring(1, 1) > "5" Then
                _ProcessDescriptiveName = process.ProcessName + " (running elevated or in another context)"

            Else
                _ProcessDescriptiveName = process.ProcessName + " (running in another context)"

            End If

        Catch ex As System.InvalidOperationException
            ' When a process has exited between the time it's first detected and its FileDescription is retrieved, that'll result in this edge case Exception.
            ' Under such circumstances, the right thing to do would be to remove the process from the array, which is something the calling code must do.
            ' Therefore, we will throw a custom Exception that the calling code can handle
            Throw New InvalidOperationException("Application has Exited or is otherwise not currently available", ex)

        Catch ex As System.ArgumentException
            ' This will catch the "Illegal characters in path" exception that results when csrss.exe or winlogon.exe is the module tested by process.MainModule.FileVersionInfo.*
            ' For csrss & winlogon, this is because System.Diagnostics.Process.MainModule.FileName returns "\??\C:\WINDOWS\system32\winlogon.exe".
            If ex.Message.StartsWith("Illegal characters in path") And (process.ProcessName = "winlogon" Or process.ProcessName = "csrss") Then
                Return process.ProcessName
            End If

        End Try

        ' Finally, strip out the unneeded or error-causing characters from the Descriptive Name
        Try
            _ValidatedProcessDescriptiveName = RemoveUnneededCharactersFromProcessDescriptiveName(_ProcessDescriptiveName)
        Catch ex As ArgumentNullException
            MessageBox.Show("Couldn't get a Descriptive Name for the process = " +
                            process.ProcessName + vbCrLf +
                            "Exception Message: " +
                            ex.Message + vbCrLf +
                            "ex.Inner: " +
                            ex.InnerException.ToString)
        End Try

        Return _ValidatedProcessDescriptiveName

    End Function

    Shared Function GetProcessName(ByVal processDescriptiveName As String) As String
        ' Using the input, this function returns the EnumeratedProcess.ProcessName property

        Dim _ProcessName As String
        ' Initialize _ProcessName to a dummy value
        _ProcessName = String.Empty

        For Each _Process As EnumeratedProcess In EnumeratedProcesses
            If (_Process.ProcessDescriptiveName = processDescriptiveName) Then
                Return _Process.ProcessName
                Exit Function
            End If
        Next

        If (_ProcessName = Nothing Or _ProcessName = String.Empty) Then
            Throw New System.ArgumentException("Argument processDescriptiveName cannot be resolved to a ProcessName")

            ' TODO: add an appropriate, somewhat generic (e.g. System.UnhandledException?) 
        Else

            ' I don't know what other conditions could exist, but better safe than sorry and I'd hate to generate another unhandled exception.
            ' Throw New 
            Return String.Empty
        End If

    End Function

    ''' <summary>
    ''' This function returns the DOMAIN\username that owns the passed-in Process.
    ''' </summary>
    ''' <param name="process"></param>
    ''' <returns>DOMAIN\username of the process owner.</returns>
    ''' <remarks></remarks>
    Shared Function GetProcessOwnerFromWmi(ByVal process As Process) As String

        Dim _ProcessID As Integer
        Dim _ProcessUserName As String = String.Empty ' initialized to avoid a warning in Visual Studio
        Dim _ProcessUserDomain As String = String.Empty ' initialized to avoid a warning in Visual Studio

        ' Validate that the proc parameter isn't null, since the function is public, so that this code is less likely to be "hacked"
        If process Is Nothing Then
            Throw New ArgumentNullException("process")
        End If

        ' Once validated, it's safe to instantiate _ProcessID
        _ProcessID = process.Id

        ' Select queryable WMI object for that Process:
        Dim _Searcher As New Management.ManagementObjectSearcher("SELECT * FROM Win32_Process WHERE ProcessId = " & _ProcessID)

        For Each _queryObj As Management.ManagementObject In _Searcher.Get
            Dim _ReturnedElements As Management.ManagementBaseObject = _queryObj.InvokeMethod("GetOwner", Nothing, Nothing)
            Try
                _ProcessUserName = _ReturnedElements("User").ToString
                _ProcessUserDomain = _ReturnedElements("Domain").ToString
            Catch ex As NullReferenceException ' This normally occurs when it's a system process we're checking
                Return "NTAUTHORITY\LOCALSYSTEM" ' I hope this doesn't cause major problems later on...
            End Try
        Next

        ' If the process for some reason doesn't return an Owner value, then let's avoid returning just "\" to the calling function
        If (_ProcessUserDomain = String.Empty Or _ProcessUserName = String.Empty) Then
            Return String.Empty
        Else
            Return _ProcessUserDomain + "\" + _ProcessUserName
        End If

    End Function

    ''' <summary>
    ''' Generic function to query WMI for any ProcessAttribute property and return its value.
    ''' </summary>
    ''' <param name="process">
    ''' The Process object to be queried (as a Process object).
    ''' </param>
    ''' <param name="processAttribute">
    ''' The WMI object to be queried for that process (as a String).
    ''' </param>
    ''' <returns>The value of the process' Property, as a String.</returns>
    ''' <remarks></remarks>
    Shared Function GetProcessPropertyFromWmi(ByRef process As Process, ByVal processAttribute As String) As String

        ' Validate that the proc parameter isn't null, since the function is not private, so that this code is less likely to be "hacked"
        If process Is Nothing Then
            Throw New ArgumentNullException("process")
        End If

        Dim _ReturnValue As String = String.Empty ' Explicitly setting _ReturnValue to String.Empty because the IDE warns I may not always set a _ReturnValue via the For Each loop
        Dim _ProcessID As Integer
        _ProcessID = process.Id
        ' Instantiate a queryable WMI object for the process
        ' RESEARCH: is there a more elegant SELECT parameter than "*" to retrieve the intended process?  Even though all process IDs have to be unique, it still seems like a bad idea to "SELECT *" if there was a better way to do this such as "SELECT 1st" or "SELECT the" [if there are such parameters].
        Dim _Searcher As New Management.ManagementObjectSearcher("SELECT * FROM Win32_Process WHERE ProcessId = " & _ProcessID)

        ' RESEARCH: is there a more efficient way to get the Command Line for a process than to have this loop through *all* Management.ManagementObjects for the Process?  That's a lot of excess cycles I'd rather not waste.
        For Each _queryObj As Management.ManagementObject In _Searcher.Get
            _ReturnValue = _queryObj(processAttribute)
        Next _queryObj

        ' This should throw an exception if no CommandLine (or other processAttribute) is retrieved
        If _ReturnValue = Nothing Then
            ' This function fails to retrieve a Command Line when passed a reference to e.g. the System process, since the System process *has* no command line.
            ' Since a process without a Command Line will never be cached anyway, throw an exception to be clear to the calling code.
            Throw New ArgumentException("This argument has no command line", process.ToString)

            ' I would normally add Exit Function here, but it shouldn't be necessary since this If...End If is the final statement in the function.

        Else

            Return _ReturnValue

        End If

    End Function

    ''' <summary>
    ''' Extracts all entries from HKLM and HKCU CurrentVersion\Run keys in the Registry.
    ''' </summary>
    ''' <param name="hive"></param>
    ''' <param name="key"></param>
    ''' <returns>A collection of strings, each representing a command line that appears in the Registry.</returns>
    ''' <remarks></remarks>
    Private Shared Function GetRunKeyCommandLines(ByVal hive As Microsoft.Win32.RegistryKey, ByVal key As String) As Collection
        ' TODO: Performance task - in future version, chop out the various parts of the CommandLine that aren't the ProcessName
        Dim RegistryValues As New Collection
        Dim _Key As Microsoft.Win32.RegistryKey

        _Key = hive.OpenSubKey(key)

        If _Key IsNot Nothing AndAlso _Key.ValueCount > 0 Then

            Dim _Values As String() = _Key.GetValueNames
            For Each _Value As String In _Values
                ' TODO: There can be Registry entries whose data types are REG_EXPAND_SZ - where should those be expanded, if anyplace?   Can't do it here when the datatype for the variable is String.

                ' Test for null because the Registry value's data may be empty - e.g. in cases where Sysinternals Autoruns was used to disable certain entries
                If _Value IsNot String.Empty Then
                    RegistryValues.Add(_Key.GetValue(_Value))
                End If
            Next

        End If

        Return RegistryValues
    End Function

    ''' <summary>
    ''' Filters out those processes that are already being started at every Windows startup and/or user logon.
    ''' </summary>
    ''' <param name="processes">Shared Hashtable collection of processes to test and potentially filter.</param>
    ''' <remarks></remarks>
    Private Shared Sub HideProcessesAlreadyAutoStarted(ByRef processes As Hashtable)
        ' Pulls in all values from HKLM Run & HKCU Run keys, determines if any of the candidate processes are being auto-started, and eliminates any matches.

        Const _HKLMRunKey As String = "Software\Microsoft\Windows\CurrentVersion\Run"
        Const _HKCURunKey As String = "Software\Microsoft\Windows\CurrentVersion\Run"
        Dim _CurrentUserHive As Microsoft.Win32.RegistryKey
        Dim _LocalMachineHive As Microsoft.Win32.RegistryKey
        Dim _AutoStartCommandLines As Collection

        _CurrentUserHive = Microsoft.Win32.Registry.CurrentUser
        _LocalMachineHive = Microsoft.Win32.Registry.LocalMachine

        ' Validate input on this public method
        If processes Is Nothing Then
            Throw New ArgumentNullException("processes")
        ElseIf processes.Count <> 0 Then

            _AutoStartCommandLines = New Collection()

            ' Gathering all command lines from both the HKLM & HKCU Run keys into one collection will make it more efficient to compare the ProcessName to all Command Lines
            For Each _CommandLine As String In GetRunKeyCommandLines(_LocalMachineHive, _HKLMRunKey)
                _AutoStartCommandLines.Add(_CommandLine)
            Next

            For Each _CommandLine As String In GetRunKeyCommandLines(_CurrentUserHive, _HKCURunKey)
                _AutoStartCommandLines.Add(_CommandLine)
            Next

            ' Now this will loop through each Process passed in and see if its ProcessName appears in any of the Command Lines
            ' TODO: investigate whether this routine will ever give us a false positive - i.e. whether the ProcessName will likely/ever occur in other non-Process Name parts of the Command Line
            For Each _Process As EnumeratedProcess In processes.Values
                For Each _CommandLine As String In _AutoStartCommandLines
                    ' This conditional routine will remove the process from ProcessesToShowUser if it is already being started via one of the enumerated Auto-Start locations

                    ' Note: here we're using the String.IndexOf() method rather than String.Contains() method to optimize the case-insensitive comparison, since Process.ProcessName is often Uppercase, and the Command Line is mixed-case
                    ' This technique was suggested here: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=311490&SiteId=1
                    Dim _index As Integer
                    _index = _CommandLine.IndexOf(_Process.ProcessName, 0, StringComparison.OrdinalIgnoreCase)
                    ' _index can be "-1" if the ProcessName string doesn't occur in _CommandLine, so test that _index is at least = 0
                    If _index >= 0 Then

                        ' TODO: to address WorkItem 3435, here's where to implement some tagging of the EnumeratedProcess class objects to be displayed as Read-Only
                        ' This'll require doing something like adding a ReadOnly boolean entry to the EnumeratedProcess class.
                        _Process.IsRemovedProgrammatically = True
                        Exit For
                    End If
                Next
            Next

            '' RESEARCH: would it be better to pull in the existing Registry values and make the comparison in memory [= memory bloat], or compare each process to the Registry values directly [= multiple Registry reads]?

            ' TODO: use the Contains() method for the String class to determine if the Process Name is contained within the Command Line of any of the auto-started apps.

        End If

    End Sub

    ''' <summary>
    ''' Filters out those processes that were not started by the user in whose context the current application is running.
    ''' </summary>
    ''' <param name="processes">Shared Hashtable collection of processes to test and potentially filter.</param>
    ''' <remarks></remarks>
    Private Shared Sub HideProcessesNotStartedByCurrentUser(ByRef processes As Hashtable)

        ' Setup username comparison variables:
        Dim _MyUsername As String
        _MyUsername = My.User.Name

        Dim _ProcessUserName As String = String.Empty

        ' Validate input on this public method
        If processes Is Nothing Then
            Throw New ArgumentNullException("processes")
        End If

        ' Enumerate all processes, then check whether each process was started by the current user:
        For Each _Process As EnumeratedProcess In processes.Values
            ' Compare the Process' UserName, stored as the .ProcessOwner property, to the user context for the current application.
            ' If it's the same, then we assume the process was started by the logged-in user, and should be considered a candidate for auto-restart i.e. left in the collection:
            If _Process.ProcessOwner <> _MyUsername Then
                _Process.IsRemovedProgrammatically = True
            End If
        Next _Process

    End Sub

    ''' <summary>
    ''' Filters out those processes that do not have a Window that is visible to the end user (e.g. present in the [Alt-[Tab] set of icons).
    ''' </summary>
    ''' <param name="processes">Shared Hashtable collection of processes to test and potentially filter.</param>
    ''' <remarks></remarks>
    Private Shared Sub HideProcessesWithoutVisibleWindows(ByRef processes As Hashtable)

        ' TODO: Research any managed code Properties that could be used include Process.StartInfo.CreateNoWindow & P.S.WindowStyle

        ' Validate input on this public method
        If processes Is Nothing Then
            Throw New ArgumentNullException("processes")
        End If

        ' Remove those processes whose Window is not visible:
        For Each _Process As EnumeratedProcess In processes.Values
            If _Process.IsWindowVisible = False Then
                _Process.IsRemovedProgrammatically = True
            End If
        Next _Process

    End Sub

    ''' <summary>
    ''' Filters out those processes that lack any windows.
    ''' </summary>
    ''' <param name="processes"></param>
    ''' <remarks></remarks>
    Private Shared Sub HideProcessesWithoutWindows(ByRef processes As Hashtable)

        ' Validate input on this public method
        If processes Is Nothing Then
            Throw New ArgumentNullException("processes")
        End If

        For Each _Process As EnumeratedProcess In processes.Values
            If _Process.HasWindow = False Then
                _Process.IsRemovedProgrammatically = True
            End If
        Next _Process

    End Sub

    ''' <summary>
    ''' Compares the collection of candidate processes to the array of apps that are configured to be hidden, and toggles the IsRemovedProgrammatically property if a match is found.
    ''' </summary>
    ''' <param name="processes">Shared Hashtable collection of processes to test and potentially filter.</param>
    ''' <param name="appsNeverDisplayed"></param>
    ''' <remarks></remarks>
    Private Shared Sub HideWellKnownProcesses(ByRef processes As Hashtable, ByVal appsNeverDisplayed As Hashtable)

        Dim _Process As EnumeratedProcess

        ' Validate input on this public method
        If processes Is Nothing Then
            Throw New ArgumentNullException("processes")

        ElseIf appsNeverDisplayed Is Nothing Then
            Throw New ArgumentNullException("appsNeverDisplayed")

        ElseIf processes.Count <> 0 Then

            'For Each _Process As EnumeratedProcess In processes.Values
            '    For Each _App As String In appsNeverDisplayed
            '        If _App.Contains(_Process.ProcessName) Then
            '            _Process.IsRemovedProgrammatically = True
            '        End If
            '    Next _App
            'Next _Process

            ' Since processes is a HashTable, we can index directly to the _Process
            For Each _App As String In appsNeverDisplayed.Values
                Try
                    _Process = processes.Item(_App)
                    _Process.IsRemovedProgrammatically = True

                Catch ex As NullReferenceException
                    ' The _App does not exist as a _Process in the processes HashTable - that's cool, just move on to the next one
                End Try
            Next

        End If

    End Sub

    ''' <summary>
    ''' Removes characters from the Process' DescriptiveName that may cause string manipulation errors elsewhere.
    ''' </summary>
    ''' <param name="processDescriptiveName"></param>
    ''' <returns></returns>
    ''' <remarks>
    ''' The purpose here is to ensure that all String manipulation including populating and reading the Registry 
    ''' won't puke on any illegal characters, e.g. +, :, (, ).  These characters are present in the Descriptive
    ''' Name for Notepad++, for example.
    ''' </remarks>
    Private Shared Function RemoveUnneededCharactersFromProcessDescriptiveName(ByVal processDescriptiveName) As String
        ' The only reason this function is Shared is to resolve an error in its use by GetProcessDescriptiveName()

        Dim _ProcessDescriptiveNameRaw As String
        Dim _ProcessDescriptiveNameFiltered As String

        ' Validate input on this non-private method
        If processDescriptiveName Is Nothing Then
            Throw New ArgumentNullException("processDescriptiveName")
        End If

        _ProcessDescriptiveNameRaw = processDescriptiveName

        ' TODO: add a function here that strips all characters out of the Descriptive Name that aren't necessary
        _ProcessDescriptiveNameFiltered = _ProcessDescriptiveNameRaw

        Return _ProcessDescriptiveNameFiltered

        ' Based on my limited understanding of how Shared functions work, it seems like the smart thing to do (since
        '   all instances of the Class share the one instance of the function and some/all of its variables) is to
        '   zeroize the values for the variables as soon as I'm done using them.
        _ProcessDescriptiveNameRaw = String.Empty
        _ProcessDescriptiveNameFiltered = String.Empty

    End Function

#End Region

#Region " Properties "

#End Region

    Private Shared Sub HideWellKnownProcesses(ByVal EnumeratedProcesses As Hashtable, ByVal HiddenApps As Collection)
        Throw New NotImplementedException
    End Sub
End Class